package time.compiler.query;
import time.Errors;
import time.compiler.query.solvers.Build;
import time.compiler.query.Data;
import time.compiler.query.Parser;
import time.compiler.types.Data;
import time.providers.base.api.BaseApi;

using time.compiler.types.Data;

/**
 *  A solver is a structure that can provide the addition of functionality or types in a loosely coupled way.
 *  This way we can have extensions that add up functionality to already defined types (e.g. like() functionality on strings)
 *  
 * @author waneck
 */


class Solver 
{
	/**
	 * 	Fields are encoded in a somewhat obscure, but memory-friendly way. Using enums would be an option, but
	 *  that would mean adding real support for type parameters, which aren't in the agenda right now.
	 *  
	 *  Each field will be an Array of Dynamics. This array represents the function types, with the last one being
	 *  the return type.
	 *  
	 *  The Dynamics can either be directly a DBType (meaning that the field has a defined type), 
	 *  or it can be an int (meaning it's a type parameter). 
	 *  	The ints can also have the bit 0x100 set, to indicate that the type will be a DBToMany of the actual type param
	 *  	Relative Type 0 is reserved, and Relative Type 1 is meant for class type parameter matches
	 *  	The special string "repeat" can be used to denote that the arguments can be repeated indefinately
	 */
	private var fields:Hash<Array<Dynamic>>;
	private var matchedTypeBuilder:Array<String>;
	
	public var cname(default, null):String;
	
	public function new()
	{
		fields = new Hash();
		this.cname = Type.getClassName(Type.getClass(this));
	}
	
	/**
	 *  Registers itself into the solver container, with the matched types got from the Build Macro
	 *  @param c	solver container instance
	 */
	public function register(c:SolverContainer) : Void
	{
		if (null == matchedTypeBuilder || matchedTypeBuilder.length == 0)
			throw "Solver doesn't have any matched type associated with it!";
		
		var h:Hash<Solver> = untyped c.s;
		for (mt in matchedTypeBuilder)
			h.set(mt, this);
	}
	
	/**
	 * Helps the parser infer anonymous function arguments if they aren't explicitly defined.
	 * 
	 * e.g. query(Test).map(function(t) t.a); //"t" isn't explicitly defined and without help won't be able to resolve "t.a"
	 * 
	 * @param call 		the called function name
	 * @param argNum	the argument number
	 * @param args		the unresolved function arguments
	 * @return 			if was succesful
	 */
	public function inferArgument(t:DBType, call:String, argNum:Int, args:Array<{ref:DBType}>):Bool
	{
		var f = fields.get(call);
		if (null == f)
			return false;
		
		if (f.length > (argNum + 1))
		{
			var f = f[argNum];
			var i = 0;
			if (!Std.is(f, Array))
				return false; //if it isn't an array, then it's a field, not a function
			
			var f:Array<Dynamic> = f;
			var len = f.length;
			var repeat = len >= 3 && f[len - 2] == "repeat";
			
			var relativeTypeBuilder = new IntHash();
			switch(t.follow())
			{
				case DBToMany(t):
					relativeTypeBuilder.set(1, t);
				default:
					relativeTypeBuilder.set(1, t);
			}
			
			for (arg in args)
			{
				if (null == arg)
					continue;
				
				var f:Dynamic = if (repeat) f[i % (len - 2)] else f[i];
				if (null == f)
					return false;
				
				var t = unwrapField(relativeTypeBuilder, f, { t : DBLazy(arg) } );
				
				i++;
			}
			
		} else {
			return false; //not a function
		}
		
		return true;
	}
	
	function mkDescend(e:QueryExpr, field:String, t:DBType, mt:MemberKind) : QueryExpr
	{
		return {
			pos : e.pos,
			expr : QDescend(e, field, mt),
			t : t
		};
	}
	
	function mkCall(e:QueryExpr, field:String, args:Array<QueryExpr>, t:DBType) : QueryExpr
	{
		return {
			pos : e.pos,
			expr : QCall(e, field, args, this.cname),
			t : t
		};
	}
	
	function unwrapField(relativeTypeBuilder:IntHash<DBType>, fieldWrap:Dynamic, arg:{t:DBType})
	{
		var c:Dynamic = fieldWrap;
		
		//if the wrapped argument is a dbtype, it is expected to be exactly this type
		if (Std.is(c, DBType))
		{
			//if the type is not known, it will be set as the wrapped type
			if (null == arg.t.follow())
			{
				arg.t = arg.t.setRef(c);
			} else {
				//if the type is known, it will be unified
				arg.t = Parser.unify(arg.t, c);
			}
		} else if (Std.is(c, Int))
		{
			//if it is an int, it means that we should get the value from relativeTypeBuilder
			
			var isToMany = false;
			if (c & 0x100 != 0)
			{
				isToMany = true;
				c = c & (0xff);
			}
			
			//if it is not set, we will set with the value from arg
			if (!relativeTypeBuilder.exists(c))
			{
				var ct = arg.t;
				relativeTypeBuilder.set(c, ct);
				//TODO check if arg.t is already a To Many relation
				if (isToMany)
					arg.t = DBToMany(arg.t);
			} else {
				var rt = relativeTypeBuilder.get(c);
				Parser.unify(rt, arg.t);
				//TODO check if arg.t is already a To Many relation
				if (isToMany)
					arg.t = DBToMany(arg.t);
			}
		} else {
			throw PError("Invalid wrapped argument: " + c, untyped arg.pos);
		}
		
	}
	
	/**
	 * Resolves a field  access
	 * @param	expr
	 * @param	field
	 * @return			if handler found, the built QueryExpr. If not found, null
	 */
	public function field(expr:QueryExpr, field:String):Null<QueryExpr>
	{
		var type = expr.t.follow();
		return switch(type)
		{
			case DBInst(c):
				if (c.members.exists(field))
				{
					var member = c.members.get(field);
					switch(member.kind)
					{
						case MValueType:
							mkDescend(expr, field, member.type, member.kind);
						case MRelation(relation):
							switch(relation)
							{
								case ParentToChild, ChildToParent(_), ChildrenToParent(_):
									mkDescend(expr, field, member.type, member.kind);
								case ParentToChildren:
									mkDescend(expr, field, member.type, member.kind);
							}
						case MReference(_), MBackReference(_,_,_):
							//TODO add check to see if platform supports referencing
							mkDescend(expr, field, member.type, member.kind);
						case MSerialized:
							throw PError("A serialized instance cannot be queried", expr.pos);
					}
				} else {
					null;
				}
			case DBToMany(_), DBBasic(_):
				null;
			case DBEnum(e):
				switch(field)
				{
					case "name", "enumName": //the same as length, but with EnumName() call
						{
							pos : expr.pos,
							expr : QBuiltin(BEnumName, [expr]),
							t : DBBasic(BVarchar(-1))
						}
					default: //TODO support accessing enums as properties, like myEnum.EnumType[1]
						throw PError("Field \"" + field + "\" not found at " + e, expr.pos);
				}
			case DBLazy(_): throw Assert;
		}
	}
	
	/**
	 * Resolves a field call
	 * @param	expr
	 * @param	field
	 * @param	calls
	 * @return			if handler found, the built QueryExpr. If not found, null
	 */
	public function call(expr:QueryExpr, field:String, calls:Array<QueryExpr>):Null<QueryExpr>
	{
		var f = fields.get(field);
		if (null == f)
			return null;
		
		if (f.length <= 1)
			throw PError("Cannot call non-function field " + field, expr.pos);
		
		var len = f.length;
		var repeat = len >= 3 && f[len - 2] == "repeat";
		var i = 0;
		
		var relativeTypeBuilder = new IntHash();
		switch(expr.t.follow())
		{
			case DBToMany(t):
				relativeTypeBuilder.set(1, t);
			default:
				relativeTypeBuilder.set(1, expr.t);
		}
		
		//check each argument in expr
		for (call in calls)
		{
			//get current wrapped code
			var cur = if (repeat) f[i % (len - 2)]; else if (i < (len - 1)) f[i]; else throw PError("Too many arguments", expr.pos);
			
			//wrapped code array => means that an anonymous function is expected
			if (Std.is(cur, Array))
			{
				//current wrapped code is an array
				var cur:Array<Dynamic> = cur;
				var qfun = switch(call.expr)
				{
					case QFunction(f): f;
					default: throw PError("Anonymous function expected", call.pos);
				};
				
				var j = 0;
				
				var len = cur.length;
				var repeat = len >= 3 && cur[len - 2] == "repeat";
				//for each wrapped argument
				for (arg in qfun.args)
				{
					var c:Dynamic = if (repeat) cur[j % (len - 2)]; else if (j < (len - 1)) cur[j]; else throw PError("Too many arguments", call.pos);
					unwrapField(relativeTypeBuilder, c, arg);
					
					j++;
				}
				
				//now do the same with the return type
				var ret = { t: qfun.ret };
				unwrapField(relativeTypeBuilder, cur[len - 1], ret);
				qfun.ret = ret.t;
			} else {
				unwrapField(relativeTypeBuilder, cur, call);
			}
			
			i++;
		}
		
		var retw = f[len - 1];
		var ret = { t:DBLazy( { ref:null } ) };
		unwrapField(relativeTypeBuilder, retw, ret);
		
		return mkCall(expr, field, calls, ret.t);
	}
	
	function getFunType(expr)
	{
		switch(expr.expr)
		{
			case QFunction(f):
				return f.ret;
			default:
				throw PError("Expected QFunction, got " + expr.expr, expr.pos);
		}
	}
	
	/**
	 * to use with EDisplay; will show all available fields for auto-completion
	 * @param	type
	 * @return
	 */
	public function display(type:DBType):Null<Array<{name:String, type:DBType}>>
	{
		//TODO complete display api
		return null;
	}
}

typedef SolverContainer = DBTypeContainer<Solver>